/*
Copyright 2023 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package google

import (
	"context"
	"errors"
	"fmt"
	"os"

	"golang.org/x/oauth2"
	googleOAuth "golang.org/x/oauth2/google"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/metadata"

	pb "sigs.k8s.io/prow/pkg/gangway"
	gangwayClient "sigs.k8s.io/prow/pkg/gangway/client"
)

// This is the client library for Go clients that need to access to the Prow
// API, aka Gangway, when Gangway is deployed in a GKE cluster and integrated
// with Cloud Endpoints.
//
// Go clients need to always append 2 things to the metadata of a gRPC call:
//
//  	1. The JWT (authentication) token (to make the call identify itself as
//  	an allowlisted client in our api_config_auth.yaml configuration for Cloud
//  	Endpoints [1]), and
//
//  	2. The API key (that is generated by the client's GCP Project).
//
//  The JWT token is generated from a GCP Service Account key file (JSON). The
//  API key is generated from the GCP user interface, like this:
//  https://cloud.google.com/docs/authentication/api-keys#create.
//
//  For us, the clients must supply the service account JSON key file, API key,
//  audience, and finally the address where Gangway is being served. An example
//  client application using this library is provided in the Prow codebase under
//  prow/gangway/example/main.go.
//
// [1]: https://github.com/GoogleCloudPlatform/golang-samples/blob/e888c56cb843f475db4f79b391be999518e63db4/endpoints/getting-started-grpc/README.md#configuring-authentication-and-authenticating-requests

type Client struct {
	// JWT token-based authentication and GCP Project identification.
	keyBytes    []byte
	audience    string
	tokenSource oauth2.TokenSource

	// apiKey identifies the GCP Project.
	apiKey string

	addr string
	conn *grpc.ClientConn

	// Include common client methods as well.
	gangwayClient.Common
}

// NewFromFile creates a Gangway client from a JSON service account key file and an audience string.
func NewFromFile(addr, keyFile, audience, clientPem, apiKey string) (*Client, error) {
	keyBytes, err := os.ReadFile(keyFile)
	if err != nil {
		return nil, fmt.Errorf("Unable to read service account key file %s: %v", keyFile, err)
	}

	return New(addr, keyBytes, audience, clientPem, apiKey)
}

// New creates a new gRPC client. It does most of the work in NewFromFile().
func New(addr string, keyBytes []byte, audience, clientPem, apiKey string) (*Client, error) {
	c := Client{}

	creds, err := credentials.NewClientTLSFromFile(clientPem, "")
	if err != nil {
		return nil, fmt.Errorf("could not process clientPem credentials: %v", err)
	}

	c.addr = addr

	conn, err := grpc.NewClient(c.addr, grpc.WithTransportCredentials(creds))
	if err != nil {
		return nil, fmt.Errorf("could not connect to %q: %v", c.addr, err)
	}
	c.conn = conn
	c.GRPC = pb.NewProwClient(c.conn)

	if len(audience) == 0 {
		return nil, errors.New("audience cannot be empty")
	}

	c.audience = audience

	if len(apiKey) == 0 {
		return nil, errors.New("apiKey cannot be empty")
	}

	c.apiKey = apiKey

	if len(keyBytes) == 0 {
		return nil, errors.New("keyBytes cannot be empty")
	}

	c.keyBytes = keyBytes

	tokenSource, err := googleOAuth.JWTAccessTokenSourceFromJSON(c.keyBytes, c.audience)
	if err != nil {
		return nil, fmt.Errorf("could not create tokenSource: %v", err)
	}

	c.tokenSource = tokenSource

	return &c, nil
}

// MkToken generates a new JWT token with a 1h TTL. This is apparently a
// cheap operation, according to
// https://github.com/GoogleCloudPlatform/golang-samples/blob/e7a5459d85661a35c5eb4f0b5759b7b30ac6ff90/endpoints/getting-started-grpc/client/main.go#L81-L88.
func (c *Client) MkToken() (string, error) {
	jwt, err := c.tokenSource.Token()
	if err != nil {
		return "", fmt.Errorf("could not generate JSON Web Token: %v", err)
	}

	return jwt.AccessToken, nil
}

// EmbedCredentials is used to modify a provided context so that it has the the
// necessary token and apiKey attached to it in the metadata.
func (c *Client) EmbedCredentials(ctx context.Context) (context.Context, error) {
	ctxWithCreds := metadata.AppendToOutgoingContext(ctx, "x-api-key", c.apiKey)

	token, err := c.MkToken()
	if err != nil {
		return ctxWithCreds, err
	}

	fmt.Printf("using token %q\n", token)

	ctxWithCreds = metadata.AppendToOutgoingContext(ctxWithCreds, "Authorization", fmt.Sprintf("Bearer %s", token))

	return ctxWithCreds, nil
}

func (c *Client) Close() {
	c.conn.Close()
}

type ClientInsecure struct {
	projectNumber string

	addr string
	conn *grpc.ClientConn

	// Include common client methods as well.
	gangwayClient.Common
}

func NewInsecure(addr, projectNumber string) (*ClientInsecure, error) {
	c := ClientInsecure{}

	c.addr = addr
	c.projectNumber = projectNumber

	// Set up a connection to gangway.
	conn, err := grpc.NewClient(c.addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		return nil, fmt.Errorf("could not connect to %q: %v", c.addr, err)
	}

	c.conn = conn
	c.GRPC = pb.NewProwClient(c.conn)

	return &c, nil
}

func (c *ClientInsecure) EmbedProjectNumber(ctx context.Context) context.Context {
	md := []string{
		"x-endpoint-api-consumer-type", "PROJECT",
		"x-endpoint-api-consumer-number", c.projectNumber,
	}

	ctx = metadata.NewOutgoingContext(
		ctx,
		metadata.Pairs(md...),
	)

	return ctx
}

func (c *ClientInsecure) Close() {
	c.conn.Close()
}
